/*
 * Tigase Jabber/XMPP Server
 * Copyright (C) 2004-2012 "Artur Hefczyc" <artur.hefczyc@tigase.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. Look for COPYING file in the top folder.
 * If not, see http://www.gnu.org/licenses/.
 *
 * $Rev: 2996 $
 * Last modified by $Author: wojtek $
 * $Date: 2012-08-21 06:29:57 +0800 (Tue, 21 Aug 2012) $
 */

package tigase.stats;

//~--- non-JDK imports --------------------------------------------------------

import tigase.conf.ConfiguratorAbstract;

import tigase.disco.ServiceEntity;
import tigase.disco.ServiceIdentity;

import tigase.server.AbstractComponentRegistrator;
import tigase.server.Command;
import tigase.server.Iq;
import tigase.server.Packet;
import tigase.server.ServerComponent;

import tigase.sys.ShutdownHook;
import tigase.sys.TigaseRuntime;

import tigase.util.ElementUtils;

import tigase.xml.Element;
import tigase.xml.XMLUtils;

import tigase.xmpp.BareJID;
import tigase.xmpp.JID;
import tigase.xmpp.StanzaType;

//~--- JDK imports ------------------------------------------------------------

import java.lang.management.ManagementFactory;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.management.ObjectName;

//~--- classes ----------------------------------------------------------------

/**
 * Class StatisticsCollector
 * 
 * 
 * Created: Tue Nov 22 07:07:11 2005
 * 
 * @author <a href="mailto:artur.hefczyc@tigase.org">Artur Hefczyc</a>
 * @version $Rev: 2996 $
 */
public class StatisticsCollector extends
		AbstractComponentRegistrator<StatisticsContainer> implements ShutdownHook {

	/**
	 *
	 */
	public static final String STATS_ARCHIVIZERS = "--stats-archiv";
	public static final String STATS_HISTORY = "--stats-history";

	/**
	 *
	 */
	public static final String STATS_ARCHIVIZERS_PROP_KEY = "stats-archiv";
	public static final String STATS_HISTORY_SIZE_PROP_KEY = "stats-history-size";
	public static final int STATS_HISTORY_SIZE_PROP_VAL = 8640;
	public static final String STATS_UPDATE_INTERVAL_PROP_KEY = "stats-update-interval";
	public static final long STATS_UPDATE_INTERVAL_PROP_VAL = 10l;

	/** Field description */
	public static final String STATISTICS_MBEAN_NAME =
			"tigase.stats:type=StatisticsProvider";
	private static final String STATS_XMLNS = "http://jabber.org/protocol/stats";
	private static final Logger log = Logger.getLogger(StatisticsCollector.class.getName());

	// ~--- fields ---------------------------------------------------------------

	private ServiceEntity serviceEntity = null;
	private StatisticsProvider sp = null;
	private Map<String, StatisticsArchivizerIfc> archivizers =
			new ConcurrentSkipListMap<String, StatisticsArchivizerIfc>();
	private ArchivizerRunner arch_runner = new ArchivizerRunner();

	// private ServiceEntity stats_modules = null;
	private Level statsLevel = Level.INFO;
	private Timer statsArchivTasks = new Timer("stats-archivizer-tasks", true);
	private TimerTask initializationCompletedTask = null;
	private int historySize = 0;
	private long updateInterval = 10;

	// ~--- methods --------------------------------------------------------------

	/**
	 * Method description
	 * 
	 * 
	 * @param component
	 */
	@Override
	public void componentAdded(StatisticsContainer component) {
		ServiceEntity item = serviceEntity.findNode(component.getName());

		if (item == null) {
			item =
					new ServiceEntity(getName(), component.getName(), "Component: "
							+ component.getName());
			item.addFeatures(CMD_FEATURES);
			item.addIdentities(new ServiceIdentity("automation", "command-node", "Component: "
					+ component.getName()));
			serviceEntity.addItems(item);
		}
	}

	/**
	 * Method description
	 * 
	 * 
	 * @param component
	 */
	@Override
	public void componentRemoved(StatisticsContainer component) {
	}

	// ~--- get methods ----------------------------------------------------------

	/**
	 * Method description
	 * 
	 * 
	 * @return
	 */
	public StatisticsList getAllStats() {
		StatisticsList list = new StatisticsList(Level.ALL);

		getAllStats(list);

		return list;
	}

	/**
	 * Method description
	 * 
	 * 
	 * @param list
	 */
	public void getAllStats(StatisticsList list) {
		for (StatisticsContainer comp : components.values()) {
			getComponentStats(comp.getName(), list);
		}
		int totalQueuesWait = 0;
		long totalQueuesOverflow = 0;
		for (StatisticsContainer comp : components.values()) {
			totalQueuesWait += list.getValue(comp.getName(), "Total queues wait", 0);
			totalQueuesOverflow += list.getValue(comp.getName(), "Total queues overflow", 0L);
		}
		list.add("total", "Total queues wait", totalQueuesWait, Level.INFO);
		list.add("total", "Total queues overflow", totalQueuesOverflow, Level.INFO);
	}

	/**
	 * Method description
	 * 
	 * 
	 * @param name
	 * @param list
	 */
	public void getComponentStats(String name, StatisticsList list) {
		StatisticsContainer stats = components.get(name);

		if (stats != null) {
			stats.getStatistics(list);
		}
	}

	/**
	 * Method description
	 * 
	 * 
	 * @return
	 */
	public List<String> getComponentsNames() {
		return new ArrayList<String>(components.keySet());
	}

	/**
	 * Method description
	 * 
	 * 
	 * @param params
	 * 
	 * @return
	 */
	@Override
	public Map<String, Object> getDefaults(Map<String, Object> params) {
		Map<String, Object> defs = super.getDefaults(params);
		String statsArchivizers = (String) params.get(STATS_ARCHIVIZERS);

		if ((statsArchivizers != null) && !statsArchivizers.isEmpty()) {
			String[] archivs = statsArchivizers.split(",");

			defs.put(STATS_ARCHIVIZERS_PROP_KEY, archivs);
		}
		int hSize = historySize;
		long updateInt = updateInterval;
		String stats_history = (String) params.get(STATS_HISTORY);
		if (stats_history != null) {
			String[] st_pars = stats_history.split(",");
			try {
				hSize = Integer.parseInt(st_pars[0]);
			} catch (Exception ex) {
				log.config("Invalid statistics history size settings: " + st_pars[0]);
			}
			if (st_pars.length > 1) {
				try {
					updateInt = Long.parseLong(st_pars[1]);
				} catch (Exception ex) {
					log.config("Invalid statistics update interval: " + st_pars[1]);
				}
			}
		}
		defs.put(STATS_HISTORY_SIZE_PROP_KEY, hSize);
		defs.put(STATS_UPDATE_INTERVAL_PROP_KEY, updateInt);

		return defs;
	}

	/**
	 * Method description
	 * 
	 * 
	 * @param from
	 * 
	 * @return
	 */
	@Override
	public List<Element> getDiscoFeatures(JID from) {
		return null;
	}

	/**
	 * Method description
	 * 
	 * 
	 * @param node
	 * @param jid
	 * @param from
	 * 
	 * @return
	 */
	@Override
	public Element getDiscoInfo(String node, JID jid, JID from) {
		if ((jid != null) && getName().equals(jid.getLocalpart()) && isAdmin(from)) {
			return serviceEntity.getDiscoInfo(node);
		}

		return null;
	}

	/**
	 * Method description
	 * 
	 * 
	 * @param node
	 * @param jid
	 * @param from
	 * 
	 * @return
	 */
	@Override
	public List<Element> getDiscoItems(String node, JID jid, JID from) {
		if (isAdmin(from)) {
			if (getName().equals(jid.getLocalpart()) || getComponentId().equals(jid)) {
				List<Element> items = serviceEntity.getDiscoItems(node, jid.toString());

				if (log.isLoggable(Level.FINEST)) {
					log.log(Level.FINEST, "Processing discoItems for node: {0}, result: {1}",
							new Object[] { node, (items == null) ? null : items.toString() });
				}

				return items;
			} else {
				if (node == null) {
					Element item =
							serviceEntity.getDiscoItem(null,
									BareJID.toString(getName(), jid.toString()));

					if (log.isLoggable(Level.FINEST)) {
						log.log(Level.FINEST, "Processing discoItems, result: {0}",
								((item == null) ? null : item.toString()));
					}

					return Arrays.asList(item);
				} else {
					return null;
				}
			}
		}

		return null;
	}

	/**
	 * Method description
	 * 
	 * 
	 * @return
	 */
	@Override
	public String getName() {
		return super.getName();
	}

	/**
	 * Method description
	 * 
	 * 
	 * @param component
	 * 
	 * @return
	 */
	@Override
	public boolean isCorrectType(ServerComponent component) {
		return component instanceof StatisticsContainer;
	}

	// ~--- methods --------------------------------------------------------------

	/**
	 * Method description
	 * 
	 * 
	 * @param packet
	 * @param results
	 */
	@Override
	public void processPacket(final Packet packet, final Queue<Packet> results) {
		if (!packet.isCommand() || (packet.getType() == StanzaType.result)) {
			return;
		}

		if (log.isLoggable(Level.FINEST)) {
			log.log(Level.FINEST, "{0} command received: {1}", new Object[] {
					packet.getCommand().name(), packet });
		}

		Iq iqc = (Iq) packet;

		BareJID stanzaFromBare = iqc.getStanzaFrom().getBareJID();
		JID stanzaFrom = JID.jidInstance( stanzaFromBare );

		if ( !isAdmin( stanzaFrom ) ){
			Packet result = iqc.commandResult( Command.DataType.result );
			Command.addTextField( result, "Error", "You do not have enough permissions to manage this domain" );
			results.offer( result );
			return;
		}
		
		switch (iqc.getCommand()) {
			case GETSTATS: {

				// Element statistics = new Element("statistics");
				Element iq =
						ElementUtils.createIqQuery(iqc.getStanzaTo(), iqc.getStanzaFrom(),
								StanzaType.result, iqc.getStanzaId(), STATS_XMLNS);
				Element query = iq.getChild("query");
				StatisticsList stats = getAllStats();

				if (stats != null) {
					for (StatRecord record : stats) {
						Element item = new Element("stat");

						item.addAttribute("name",
								record.getComponent() + "/" + record.getDescription());
						item.addAttribute("units", record.getUnit());
						item.addAttribute("value", record.getValue());
						query.addChild(item);
					} // end of for ()
				} // end of if (stats != null && stats.count() > 0)

				Packet result = Packet.packetInstance(iq, iqc.getStanzaTo(), iqc.getStanzaFrom());

				// Command.setData(result, statistics);
				results.offer(result);

				break;
			}

			case OTHER: {
				if (iqc.getStrCommand() == null) {
					return;
				}

				String nick = iqc.getTo().getLocalpart();

				if (!getName().equals(nick)) {
					return;
				}

				Command.Action action = Command.getAction(iqc);

				if (action == Command.Action.cancel) {
					Packet result = iqc.commandResult(null);

					results.offer(result);

					return;
				}

				String tmp_val = Command.getFieldValue(iqc, "Stats level");

				if (tmp_val != null) {
					statsLevel = Level.parse(tmp_val);

					if (log.isLoggable(Level.FINEST)) {
						log.log(Level.FINEST, "statsLevel parsed to: {0}", statsLevel.getName());
					}
				}

				StatisticsList list = new StatisticsList(statsLevel);

				if (iqc.getStrCommand().equals("stats")) {
					if (log.isLoggable(Level.FINEST)) {
						log.log(Level.FINEST, "Getting all stats for level: {0}",
								statsLevel.getName());
					}

					getAllStats(list);

					if (log.isLoggable(Level.FINEST)) {
						log.log(Level.FINEST, "All stats for level loaded: {0}", statsLevel.getName());
					}
				} else {
					String[] spl = iqc.getStrCommand().split("/");

					if (log.isLoggable(Level.FINEST)) {
						log.log(Level.FINEST, "Getting stats for component: {0}, level: {1}",
								new Object[] { spl[1], statsLevel.getName() });
					}

					getComponentStats(spl[1], list);

					if (log.isLoggable(Level.FINEST)) {
						log.log(Level.FINEST, "Stats loaded for component: {0}, level: {1}",
								new Object[] { spl[1], statsLevel.getName() });
					}
				}

				Packet result = iqc.commandResult(Command.DataType.form);

				if (list != null) {
					for (StatRecord rec : list) {
						if (rec.getType() == StatisticType.LIST) {
							Command.addFieldMultiValue(result,
									XMLUtils.escape(rec.getComponent() + "/" + rec.getDescription()),
									rec.getListValue());
						} else {
							Command.addFieldValue(result,
									XMLUtils.escape(rec.getComponent() + "/" + rec.getDescription()),
									XMLUtils.escape(rec.getValue()));
						}
					}
				}

				Command.addFieldValue(
						result,
						"Stats level",
						statsLevel.getName(),
						"Stats level",
						new String[] { Level.INFO.getName(), Level.FINE.getName(),
								Level.FINER.getName(), Level.FINEST.getName() }, new String[] {
								Level.INFO.getName(), Level.FINE.getName(), Level.FINER.getName(),
								Level.FINEST.getName() });
				results.offer(result);

				if (log.isLoggable(Level.FINEST)) {
					log.log(Level.FINEST, "Returning stats result: {0}", result);
				}

				break;
			}

			default:
				break;
		} // end of switch (packet.getCommand())
	}

	// ~--- set methods ----------------------------------------------------------

	/**
	 * Method description
	 * 
	 * 
	 * @param name
	 */
	@Override
	public void setName(String name) {
		super.setName(name);
		serviceEntity = new ServiceEntity(name, "stats", "Server statistics");
		serviceEntity.addIdentities(new ServiceIdentity("component", "stats",
				"Server statistics"), new ServiceIdentity("automation", "command-node",
				"All statistics"), new ServiceIdentity("automation", "command-list",
				"Statistics retrieving commands"));
		serviceEntity.addFeatures(DEF_FEATURES);
		serviceEntity.addFeatures(CMD_FEATURES);

	}

	/**
	 * Method description
	 * 
	 * 
	 * @param props
	 */
	@Override
	public void setProperties(Map<String, Object> props) {
		super.setProperties(props);

		String[] archivs = (String[]) props.get(STATS_ARCHIVIZERS_PROP_KEY);

		if (archivs != null) {
			initStatsArchivizers(archivs, props);
		}
		if (props.get(STATS_HISTORY_SIZE_PROP_KEY) != null) {
			historySize = (Integer) props.get(STATS_HISTORY_SIZE_PROP_KEY);
		}
		if (props.get(STATS_UPDATE_INTERVAL_PROP_KEY) != null) {
			updateInterval = (Long) props.get(STATS_UPDATE_INTERVAL_PROP_KEY);
		}

	}

	// ~--- methods --------------------------------------------------------------

	/**
	 * Method description
	 * 
	 * 
	 * @return
	 */
	@Override
	public String shutdown() {
		StatisticsList allStats = getAllStats();
		StringBuilder sb = new StringBuilder(4096);

		for (StatRecord statRecord : allStats) {
			sb.append(statRecord.toString()).append('\n');
		}

		return sb.toString();
	}

	protected void statsUpdated() {
		synchronized (arch_runner) {
			arch_runner.notifyAll();
		}
	}

	// ~--- get methods ----------------------------------------------------------

	private Map<String, Object> getArchivizerConf(String name, Map<String, Object> props) {
		Map<String, Object> result = new LinkedHashMap<String, Object>(4);
		String key_start = STATS_ARCHIVIZERS_PROP_KEY + "/" + name + "/";

		for (Map.Entry<String, Object> entry : props.entrySet()) {
			if (entry.getKey().startsWith(key_start)) {
				String key = entry.getKey().substring(key_start.length());

				log.log(Level.CONFIG, "Found {0} property: {1} = {2}", new Object[] { name, key,
						entry.getValue() });
				result.put(key, entry.getValue());
			}
		}

		return result;
	}

	@Override
	public void initializationCompleted() {
		super.initializationCompleted();
		try {
			sp = new StatisticsProvider(this, historySize, updateInterval);

			String objName = STATISTICS_MBEAN_NAME;
			ObjectName on = new ObjectName(objName);

			ManagementFactory.getPlatformMBeanServer().registerMBean(sp, on);
			ConfiguratorAbstract.putMXBean(objName, sp);
		} catch (Exception ex) {
			log.log(Level.SEVERE, "Can not install Statistics MXBean: ", ex);
		}

		TigaseRuntime.getTigaseRuntime().addShutdownHook(this);

		if (initializationCompletedTask != null) {
			initializationCompletedTask.run();
		}
	}

	private void initStatsArchivizers(final String[] archivs,
			final Map<String, Object> props) {
		for (String stat_arch_key : archivizers.keySet()) {
			StatisticsArchivizerIfc stat_arch = archivizers.remove(stat_arch_key);

			if (stat_arch != null) {
				stat_arch.release();
			}
		}

		initializationCompletedTask = new TimerTask() {
			public void run() {
				for (String arch_prop : archivs) {
					try {
						String[] arch_prop_a = arch_prop.split(":");
						String arch_class = arch_prop_a[0];
						String arch_name = arch_prop_a[1];
						final StatisticsArchivizerIfc stat_arch =
								(StatisticsArchivizerIfc) Class.forName(arch_class).newInstance();

						stat_arch.init(getArchivizerConf(arch_name, props));

						long freq = -1;

						if (arch_prop_a.length > 2) {
							try {
								freq = Long.parseLong(arch_prop_a[2]);
							} catch (Exception e) {
								freq = -1;
							}
						}

						// Some archivizers run in regular intervals of time
						// some others run each time statistics collection has completed.
						if (freq > 0) {
							statsArchivTasks.schedule(new TimerTask() {
								@Override
								public void run() {
									stat_arch.execute(sp);
								}
							}, freq * 1000, freq * 1000);
						} else {
							archivizers.put(arch_name, stat_arch);
						}

						log.log(Level.CONFIG, "Loaded statistics archivizer: {0} for class: {1}",
								new Object[] { arch_name, arch_class });
					} catch (Exception e) {
						log.log(Level.SEVERE, "Can't initialize statistics archivizer: " + arch_prop,
								e);
					}
				}
			}
		};

	}

	// ~--- inner classes --------------------------------------------------------

	private class ArchivizerRunner extends Thread {
		private boolean stopped = false;

		// ~--- constructors -------------------------------------------------------

		private ArchivizerRunner() {
			super("stats-archivizer");
			setDaemon(true);
			start();
		}

		// ~--- methods ------------------------------------------------------------

		/**
		 * Method description
		 * 
		 */
		@Override
		public void run() {
			while (!stopped) {
				try {
					synchronized (this) {
						this.wait();
					}

					for (Map.Entry<String, StatisticsArchivizerIfc> archiv_entry : archivizers
							.entrySet()) {
						archiv_entry.getValue().execute(sp);
					}
				} catch (InterruptedException ex) {

					// Ignore...
				}
			}
		}
	}
}

// ~ Formatted in Sun Code Convention

// ~ Formatted by Jindent --- http://www.jindent.com
